/************************************************************************************
Copyright : Copyright (c) Facebook Technologies, LLC and its affiliates. All rights reserved.

Licensed under the Oculus Master SDK License Version 1.0 (the "License"); you may not use
the Utilities SDK except in compliance with the License, which is provided at the time of installation
or download, or which otherwise accompanies this software in either electronic or hard copy form.

You may obtain a copy of the License at
https://developer.oculus.com/licenses/oculusmastersdk-1.0/

Unless required by applicable law or agreed to in writing, the Utilities SDK distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
ANY KIND, either express or implied. See the License for the specific language governing
permissions and limitations under the License.
************************************************************************************/
#if USING_XR_MANAGEMENT && USING_XR_SDK_OCULUS
#define USING_XR_SDK
#endif

#if UNITY_2020_1_OR_NEWER
#define REQUIRES_XR_SDK
#endif

using System;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using UnityEngine;
using System.Collections.Generic;

#if USING_XR_SDK
using UnityEngine.XR;
using UnityEngine.Experimental.XR;
#endif

using InputTracking = UnityEngine.XR.InputTracking;
using Node = UnityEngine.XR.XRNode;
using Settings = UnityEngine.XR.XRSettings;

/// <summary>
/// Manages an Oculus Rift head-mounted display (HMD).
/// </summary>
public class OVRDisplay
{
	/// <summary>
	/// Contains full fov information per eye
	/// Under Symmetric Fov mode, UpFov == DownFov and LeftFov == RightFov.
	/// </summary>
	public struct EyeFov
	{
		public float UpFov;
		public float DownFov;
		public float LeftFov;
		public float RightFov;
	}

	/// <summary>
	/// Specifies the size and field-of-view for one eye texture.
	/// </summary>
	public struct EyeRenderDesc
	{
		/// <summary>
		/// The horizontal and vertical size of the texture.
		/// </summary>
		public Vector2 resolution;

		/// <summary>
		/// The angle of the horizontal and vertical field of view in degrees.
		/// For Symmetric FOV interface compatibility
		/// Note this includes the fov angle from both sides
		/// </summary>
		public Vector2 fov;

		/// <summary>
		/// The full information of field of view in degrees.
		/// When Asymmetric FOV isn't enabled, this returns the maximum fov angle
		/// </summary>
		public EyeFov fullFov;
	}

	/// <summary>
	/// Contains latency measurements for a single frame of rendering.
	/// </summary>
	public struct LatencyData
	{
		/// <summary>
		/// The time it took to render both eyes in seconds.
		/// </summary>
		public float render;

		/// <summary>
		/// The time it took to perform TimeWarp in seconds.
		/// </summary>
		public float timeWarp;

		/// <summary>
		/// The time between the end of TimeWarp and scan-out in seconds.
		/// </summary>
		public float postPresent;
		public float renderError;
		public float timeWarpError;
	}

	private bool needsConfigureTexture;
	private EyeRenderDesc[] eyeDescs = new EyeRenderDesc[2];
	private bool recenterRequested = false;
	private int recenterRequestedFrameCount = int.MaxValue;
	private int localTrackingSpaceRecenterCount = 0;

	/// <summary>
	/// Creates an instance of OVRDisplay. Called by OVRManager.
	/// </summary>
	public OVRDisplay()
	{
		UpdateTextures();
	}

	/// <summary>
	/// Updates the internal state of the OVRDisplay. Called by OVRManager.
	/// </summary>
	public void Update()
	{
		UpdateTextures();

		if (recenterRequested && Time.frameCount > recenterRequestedFrameCount)
		{
			Debug.Log("Recenter event detected");
			if (RecenteredPose != null)
			{
				RecenteredPose();
			}
			recenterRequested = false;
			recenterRequestedFrameCount = int.MaxValue;
		}

		if (OVRPlugin.GetSystemHeadsetType() >= OVRPlugin.SystemHeadset.Oculus_Quest &&
			OVRPlugin.GetSystemHeadsetType() < OVRPlugin.SystemHeadset.Rift_DK1) // all Oculus Standalone headsets
		{
			int recenterCount = OVRPlugin.GetLocalTrackingSpaceRecenterCount();
			if (localTrackingSpaceRecenterCount != recenterCount)
			{
				Debug.Log("Recenter event detected");
				if (RecenteredPose != null)
				{
					RecenteredPose();
				}
				localTrackingSpaceRecenterCount = recenterCount;
			}
		}
	}

	/// <summary>
	/// Occurs when the head pose is reset.
	/// </summary>
	public event System.Action RecenteredPose;

	/// <summary>
	/// Recenters the head pose.
	/// </summary>
	public void RecenterPose()
	{
#if USING_XR_SDK
		XRInputSubsystem currentInputSubsystem = OVRManager.GetCurrentInputSubsystem();
		if (currentInputSubsystem != null)
		{
			currentInputSubsystem.TryRecenter();
		}
#elif !REQUIRES_XR_SDK
		InputTracking.Recenter();
#endif

		// The current poses are cached for the current frame and won't be updated immediately
		// after UnityEngine.VR.InputTracking.Recenter(). So we need to wait until next frame
		// to trigger the RecenteredPose delegate. The application could expect the correct pose
		// when the RecenteredPose delegate get called.
		recenterRequested = true;
		recenterRequestedFrameCount = Time.frameCount;

#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
		OVRMixedReality.RecenterPose();
#endif
	}

	/// <summary>
	/// Gets the current linear acceleration of the head.
	/// </summary>
	public Vector3 acceleration
	{
		get {
			if (!OVRManager.isHmdPresent)
				return Vector3.zero;

			Vector3 retVec = Vector3.zero;
			if (OVRNodeStateProperties.GetNodeStatePropertyVector3(Node.Head, NodeStatePropertyType.Acceleration, OVRPlugin.Node.Head, OVRPlugin.Step.Render, out retVec))
				return retVec;
			return Vector3.zero;
		}
	}

    /// <summary>
    /// Gets the current angular acceleration of the head in radians per second per second about each axis.
    /// </summary>
    public Vector3 angularAcceleration
    {
        get
        {
            if (!OVRManager.isHmdPresent)
				return Vector3.zero;

			Vector3 retVec = Vector3.zero;
			if (OVRNodeStateProperties.GetNodeStatePropertyVector3(Node.Head, NodeStatePropertyType.AngularAcceleration, OVRPlugin.Node.Head, OVRPlugin.Step.Render, out retVec))
				return retVec;
			return Vector3.zero;

        }
    }

    /// <summary>
    /// Gets the current linear velocity of the head in meters per second.
    /// </summary>
    public Vector3 velocity
    {
        get
        {
            if (!OVRManager.isHmdPresent)
                return Vector3.zero;

			Vector3 retVec = Vector3.zero;
			if (OVRNodeStateProperties.GetNodeStatePropertyVector3(Node.Head, NodeStatePropertyType.Velocity, OVRPlugin.Node.Head, OVRPlugin.Step.Render, out retVec))
				return retVec;
			return Vector3.zero;
		}
    }

	/// <summary>
	/// Gets the current angular velocity of the head in radians per second about each axis.
	/// </summary>
	public Vector3 angularVelocity
	{
		get {
			if (!OVRManager.isHmdPresent)
				return Vector3.zero;

			Vector3 retVec = Vector3.zero;
			if (OVRNodeStateProperties.GetNodeStatePropertyVector3(Node.Head, NodeStatePropertyType.AngularVelocity, OVRPlugin.Node.Head, OVRPlugin.Step.Render, out retVec))
				return retVec;
			return Vector3.zero;
		}
	}

	/// <summary>
	/// Gets the resolution and field of view for the given eye.
	/// </summary>
    public EyeRenderDesc GetEyeRenderDesc(UnityEngine.XR.XRNode eye)
	{
		return eyeDescs[(int)eye];
	}

	/// <summary>
	/// Gets the current measured latency values.
	/// </summary>
	public LatencyData latency
	{
		get {
			if (!OVRManager.isHmdPresent)
				return new LatencyData();

            string latency = OVRPlugin.latency;

            var r = new Regex("Render: ([0-9]+[.][0-9]+)ms, TimeWarp: ([0-9]+[.][0-9]+)ms, PostPresent: ([0-9]+[.][0-9]+)ms", RegexOptions.None);

            var ret = new LatencyData();

            Match match = r.Match(latency);
            if (match.Success)
            {
                ret.render = float.Parse(match.Groups[1].Value);
                ret.timeWarp = float.Parse(match.Groups[2].Value);
                ret.postPresent = float.Parse(match.Groups[3].Value);
            }

            return ret;
		}
	}

	/// <summary>
	/// Gets application's frame rate reported by oculus plugin
	/// </summary>
	public float appFramerate
	{
		get
		{
			if (!OVRManager.isHmdPresent)
				return 0;

			return OVRPlugin.GetAppFramerate();
		}
	}

	/// <summary>
	/// Gets the recommended MSAA level for optimal quality/performance the current device.
	/// </summary>
	public int recommendedMSAALevel
	{
		get
		{
			int result = OVRPlugin.recommendedMSAALevel;

			if (result == 1)
				result = 0;

			return result;
		}
	}

	/// <summary>
	/// Gets the list of available display frequencies supported by this hardware.
	/// </summary>
	public float[] displayFrequenciesAvailable
	{
		get { return OVRPlugin.systemDisplayFrequenciesAvailable; }
	}

	/// <summary>
	/// Gets and sets the current display frequency.
	/// </summary>
	public float displayFrequency
	{
		get
		{
			return OVRPlugin.systemDisplayFrequency;
		}
		set
		{
			OVRPlugin.systemDisplayFrequency = value;
		}
	}

	private void UpdateTextures()
	{
		ConfigureEyeDesc(Node.LeftEye);
		ConfigureEyeDesc(Node.RightEye);
	}

	private void ConfigureEyeDesc(Node eye)
	{
		if (!OVRManager.isHmdPresent)
			return;

		int eyeTextureWidth = Settings.eyeTextureWidth;
		int eyeTextureHeight = Settings.eyeTextureHeight;

		eyeDescs[(int)eye] = new EyeRenderDesc();
		eyeDescs[(int)eye].resolution = new Vector2(eyeTextureWidth, eyeTextureHeight);

		OVRPlugin.Frustumf2 frust;
		if (OVRPlugin.GetNodeFrustum2((OVRPlugin.Node)eye, out frust))
		{
			eyeDescs[(int)eye].fullFov.LeftFov = Mathf.Rad2Deg * Mathf.Atan(frust.Fov.LeftTan);
			eyeDescs[(int)eye].fullFov.RightFov = Mathf.Rad2Deg * Mathf.Atan(frust.Fov.RightTan);
			eyeDescs[(int)eye].fullFov.UpFov = Mathf.Rad2Deg * Mathf.Atan(frust.Fov.UpTan);
			eyeDescs[(int)eye].fullFov.DownFov = Mathf.Rad2Deg * Mathf.Atan(frust.Fov.DownTan);
		}
		else
		{
			OVRPlugin.Frustumf frustOld = OVRPlugin.GetEyeFrustum((OVRPlugin.Eye)eye);
			eyeDescs[(int)eye].fullFov.LeftFov = Mathf.Rad2Deg * frustOld.fovX * 0.5f;
			eyeDescs[(int)eye].fullFov.RightFov = Mathf.Rad2Deg * frustOld.fovX * 0.5f;
			eyeDescs[(int)eye].fullFov.UpFov = Mathf.Rad2Deg * frustOld.fovY * 0.5f;
			eyeDescs[(int)eye].fullFov.DownFov = Mathf.Rad2Deg * frustOld.fovY * 0.5f;
		}

		// Symmetric Fov uses the maximum fov angle
		float maxFovX = Mathf.Max(eyeDescs[(int)eye].fullFov.LeftFov, eyeDescs[(int)eye].fullFov.RightFov);
		float maxFovY = Mathf.Max(eyeDescs[(int)eye].fullFov.UpFov, eyeDescs[(int)eye].fullFov.DownFov);
		eyeDescs[(int)eye].fov.x = maxFovX * 2.0f;
		eyeDescs[(int)eye].fov.y = maxFovY * 2.0f;

		if (!OVRPlugin.AsymmetricFovEnabled)
		{
			eyeDescs[(int)eye].fullFov.LeftFov = maxFovX;
			eyeDescs[(int)eye].fullFov.RightFov = maxFovX;

			eyeDescs[(int)eye].fullFov.UpFov = maxFovY;
			eyeDescs[(int)eye].fullFov.DownFov = maxFovY;
		}
	}
}
